package cn.dansj.controller;

import cn.dansj.common.request.annotation.http.GetMethod;
import cn.dansj.common.request.annotation.http.PostMethod;
import cn.dansj.common.request.annotation.http.RequestMappingMethod;
import cn.dansj.common.request.annotation.http.RequestParam;
import cn.dansj.bean.FileUploadInfo;
import cn.dansj.common.utils.bean.ResponseMessage;
import cn.dansj.common.utils.bean.Return;
import cn.dansj.common.utils.crypt.AesUtils;
import cn.dansj.common.utils.json.DictMap;
import cn.dansj.common.utils.enums.BooleanType;
import cn.dansj.common.utils.enums.CreateBucket;
import cn.dansj.common.utils.enums.ResponseCode;
import cn.dansj.enums.GlobalEnv;
import cn.dansj.exception.InterfaceResponseException;
import cn.dansj.service.MinioUploadService;
import cn.dansj.utils.http.MimeType;
import cn.dansj.common.request.RequestUtils;
import cn.dansj.utils.minio.MinioBucketPolicy;
import cn.dansj.utils.minio.MinioUtils;
import cn.dansj.common.redis.RedisUtils;
import cn.dansj.common.utils.transfer.*;
import cn.dansj.utils.project.ProjectUtils;
import io.minio.StatObjectResponse;
import org.apache.catalina.connector.ClientAbortException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

@RequestMappingMethod("/file")
public class MinioController {
    private static final Logger logger = LoggerFactory.getLogger(MinioController.class);
    private final RedisUtils redisUtils;
    private final MinioUploadService minioUploadService;

    public MinioController(RedisUtils redisUtils, MinioUploadService minioUploadService) {
        this.redisUtils = redisUtils;
        this.minioUploadService = minioUploadService;
    }

    @PostMethod("/upload")
    public Return<?> upload(MultipartFile file, @RequestParam String bucket, @RequestParam(required = false) String filename) throws IOException {
        final boolean hasKey = redisUtils.hasKey(ProjectUtils.getBucketName(bucket));
        if (!hasKey) {
            return Return.T(false, "桶不存在");
        }
        final String fileId = minioUploadService.upload(file, bucket, filename);
        return Return.T(Verification.checkNotNull(fileId), fileId);
    }

    @GetMethod("/tag")
    public Return<?> list() {
        return Return.T(minioUploadService.getTagInfo());
    }

    @PostMethod("/tag")
    public Return<?> createTag(@RequestParam String tag) {
        CreateBucket createBucket = minioUploadService.createBucket(tag);
        return Return.T(createBucket.getStatus(), createBucket.getMessage());
    }

    @PostMethod("/quota")
    public Return<?> quota(@RequestParam String tag, @RequestParam int size, @RequestParam String unit) {
        return Return.T(ProjectUtils.getMinio().setQuota(tag, size, unit), "执行结束");
    }

    @PostMethod("/delTag")
    public Return<?> delTag(@RequestParam String tag) {
        boolean delete = minioUploadService.deleteBucket(tag);
        if (delete) {
            return Return.T(true, "删除成功");
        }
        return Return.T(false, "桶内存在文件,无法删除");
    }

    @GetMethod("/search")
    public Return<?> file(@RequestParam String tag) {
        return Return.T(minioUploadService.getFiles(tag));
    }

    @PostMethod("/policy")
    public Return<?> policy(@RequestParam String tag, @RequestParam String policy) {
        MinioBucketPolicy minioBucketPolicy = MinioBucketPolicy.get(policy);
        boolean setBucketPolicy = ProjectUtils.getMinio().setBucketPolicy(tag, minioBucketPolicy);
        return Return.T(setBucketPolicy, minioBucketPolicy.getName());
    }

    @PostMethod("/delete")
    public Return<?> delete(@RequestParam String bucket, @RequestParam String id) {
        final String bucketName = ProjectUtils.getBucketName(bucket);
        final DictMap fileObject = DictMap.from(redisUtils.hgetall(bucketName));
        final DictMap files = fileObject.getJSONObject("files");
        files.remove(id);
        fileObject.put("files", files);
        redisUtils.hmset(bucketName, fileObject);
        return Return.T(true, "文件删除成功");
    }

    @PostMethod("/modifyFilename")
    public Return<?> modifyFilename(@RequestParam String bucket, @RequestParam String filename, @RequestParam String id) {
        final String bucketName = ProjectUtils.getBucketName(bucket);
        final DictMap fileObject = DictMap.from(redisUtils.hgetall(bucketName));
        final DictMap files = fileObject.getJSONObject("files");
        final DictMap file = files.getJSONObject(id);
        file.put("fileName", filename);
        files.put(id, file);
        fileObject.put("files", files);
        redisUtils.hmset(bucketName, fileObject);
        return Return.T(true, "文件名修改成功");
    }

    @PostMethod("/moveFile")
    public Return<?> moveFile(@RequestParam String bucket, @RequestParam String newBucket, @RequestParam String fileId) {
        final String bucketName = ProjectUtils.getBucketName(bucket);
        final String newBucketName = ProjectUtils.getBucketName(newBucket);
        // 从旧的bucket中拿到文件
        final DictMap fileObject = DictMap.from(redisUtils.hgetall(bucketName));
        final DictMap files = fileObject.getJSONObject("files");
        final Object file = files.remove(fileId);
        AssertUtils.notNull(file, new InterfaceResponseException(ResponseCode.Error.getStatusCode(), "文件已不存在于" + bucket + "桶内,请重试"));
        // 把拿到的文件添加到新的bucket下
        final DictMap newFileObject = DictMap.from(redisUtils.hgetall(newBucketName));
        final DictMap newFiles = newFileObject.getJSONObject("files");
        final DictMap bucket1 = DictMap.from(file).put("bucketName", newBucket);
        newFiles.put(fileId, bucket1);
        fileObject.put("files", files);
        newFileObject.put("files", newFiles);
        // 保存到redis缓存
        redisUtils.hmset(bucketName, fileObject);
        redisUtils.hmset(newBucketName, newFileObject);
        return Return.T(true, "文件转移成功");
    }

    @GetMethod("/fileInfo")
    public Return<?> fileInfo(@RequestParam String bucket, @RequestParam String filename) {
        return Return.T(ProjectUtils.getMinio().fileInfo(bucket, filename));
    }

    @GetMethod("/share")
    public Return<?> share(@RequestParam String bucket, @RequestParam String filename, @RequestParam int maxExpireDay) {
        ResponseMessage responseMessage = ProjectUtils.getMinio().share(bucket, filename, maxExpireDay);
        return Return.T(responseMessage.getStatus(), responseMessage.getMessage());
    }

    @GetMethod(value = "/get/{bucket}/{fileInfo}", permission = BooleanType.TRUE)
    public void getFile(@RequestParam(path = true) String bucket,
                        @RequestParam(path = true) String fileInfo,
                        @RequestParam(required = false) String sign,
                        @RequestParam(required = false) boolean preview,
                        @RequestParam(required = false) boolean cache,
                        HttpServletResponse response, HttpServletRequest request) {
        if (Verification.checkAnyNull(bucket, fileInfo, sign)) {
            RequestUtils.response404();
            return;
        } else if (request.getMethod().equalsIgnoreCase("head")) {
            // 处理head探测文件是否存在的请求
            return;
        }

        boolean isFilename = fileInfo.contains(".");
        final String referer = request.getHeader("Referer");

        // 是否允许浏览器直接打开，直接打开时referer不存在
        final boolean refererAllowNull = ProjectUtils.getSettingByRedis("intercept").getBooleanValue("get.file.referer.allow.null");

        if ((Verification.checkNull(referer) && !refererAllowNull) || (Verification.checkNotNull(referer) && !inReferer(referer))) {
            List<String> verificationCode = redisUtils.keys("verificationCode@*").stream().map(redisUtils::getString).toList();
            boolean checkSignPass = true;
            if (!verificationCode.contains(sign)) {
                try {
                    DictMap decrypt = DictMap.from(AesUtils.decrypt(sign, GlobalEnv.AesKey));
                    AssertUtils.state(DateTime.getInstance(decrypt.getString("time")).getTimeInMillis() >= System.currentTimeMillis(), "时间校验失败");
                    AssertUtils.state(decrypt.getString(isFilename ? "name" : "id").equals(fileInfo), "文件名校验失败");
                    AssertUtils.state(decrypt.getString("bucket").equals(bucket), "文件桶校验失败");
                } catch (Exception exception) {
                    checkSignPass = false;
                }
            }

            // 校验签名是否在验证码中
            if (!checkSignPass) {
                RequestUtils.response404();
                RequestUtils.setResponseMessage(405, "签名验证失败, 文件下载请求被拦截");
                return;
            }
        }

        response.setCharacterEncoding("UTF-8");
        MinioUtils minio = ProjectUtils.getMinio();

        final DictMap fileMap = DictMap.from(redisUtils.hgetall(ProjectUtils.getBucketName(bucket)));
        final DictMap files = fileMap.getJSONObject("files");
        if (files == null) {
            RequestUtils.response404();
            return;
        }
        String filename = "";
        String md5 = "";

        if (isFilename) {
            filename = fileInfo;
            for (Object obj : files.values()) {
                final DictMap file = DictMap.parseObject(obj, new DictMap.JavaType<>() {
                });
                if (filename.equals(file.getString("fileName"))) {
                    md5 = file.getString("fileMd5");
                    break;
                }
            }
        } else {
            final DictMap file = files.getJSONObject(fileInfo);
            if (file != null) {
                md5 = file.getString("fileMd5");
                filename = file.getString("fileName");
            }
        }

        String minioBucket = ProjectUtils.getSettingByRedis("minio", "defaultBucket", "project");
        long maxAge = cache ? TimeUnit.DAYS.toSeconds(30) : TimeUnit.MINUTES.toSeconds(1);

        StatObjectResponse statObjectResponse = minio.fileInfo(minioBucket, md5);
        if (statObjectResponse == null) {
            RequestUtils.response404();
            return;
        }

        try (final InputStream fileContent = minio.download(minioBucket, md5)) {
            try (final OutputStream outputStream = response.getOutputStream()) {
                long size = statObjectResponse.size();

                long pos = 0, last = size - 1, sum = 0;

                if (Verification.checkNotNull(request.getHeader(HttpHeaders.RANGE))) {
                    response.setHeader("Accept-Range", "bytes");
                    response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                    response.setContentType(MimeType.APPLICATION_X_DOWNLOAD_VALUE);
                    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8));
                    String numRange = request.getHeader(HttpHeaders.RANGE).replaceAll("bytes=", "");
                    String[] strRange = numRange.split("-");
                    if (strRange.length == 2) {
                        pos = Long.parseLong(strRange[0].trim());
                        last = Long.parseLong(strRange[1].trim());
                        //若结束字节超出文件大小 取文件大小
                        if (last > size - 1) {
                            last = size - 1;
                        }
                    } else {
                        //若只给一个长度  开始位置一直到结束
                        pos = Long.parseLong(numRange.replaceAll("-", "").trim());
                    }

                    String contentRange = "bytes " + pos + "-" + last + "/" + size;
                    response.setHeader("Content-Range", contentRange);
                } else if (preview) {
                    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8));
                    response.setContentType(ProjectUtils.mediaType(filename).toString());
                    response.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=" + maxAge + ", public");
                    response.setContentLengthLong(size);
                } else {
                    response.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=" + maxAge + ", public");
                    // response.setContentType(ProjectUtils.mediaType(filename).toString());
                    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8));
                    response.setContentType(MimeType.APPLICATION_X_DOWNLOAD_VALUE);
                    response.setContentLengthLong(size);
                }

                long rangeLength = last - pos + 1;

                fileContent.skip(pos);//跳过已读的文件(重点，跳过之前已经读过的文件)
                byte[] buffer = new byte[1024];
                //相等证明读完
                while (sum < rangeLength) {
                    int length = fileContent.read(buffer, 0, (rangeLength - sum) <= buffer.length ? (int) (rangeLength - sum) : buffer.length);
                    sum = sum + length;
                    outputStream.write(buffer, 0, length);
                    outputStream.flush(); // 刷新输出流，确保数据已发送到客户端
                }
            }
        } catch (ClientAbortException abortException) {
            RequestUtils.response(ResponseCode.ClientAbortDownload);
        } catch (Exception e) {
            RequestUtils.response(ResponseCode.FileDownloadException);
        }
    }

    /**
     * 获取上传文件
     *
     * @param fileMD5 文件md5
     */
    @GetMethod("/getUploadingFile/{fileMD5}")
    public Return<?> getUploadingFile(@RequestParam(path = true) String fileMD5) {
        if (Verification.checkNull(fileMD5)) {
            return Return.T(false, "md5值不能为空");
        }

        FileUploadInfo fileUploadInfo = DictMap.parseObject(redisUtils.hgetall(fileMD5), FileUploadInfo.class);

        if (fileUploadInfo != null) {
            // 查询上传后的分片数据
            fileUploadInfo.setChunkUploadedList(ProjectUtils.getMinio().getChunkByFileMD5(fileUploadInfo.getFileName(), fileUploadInfo.getUploadId(), fileUploadInfo.getFileType()));
            return Return.T(fileUploadInfo);
        }
        return Return.T(false, "文件信息不存在");
    }

    /**
     * 校验文件是否存在
     *
     * @param md5 String
     * @return ResponseResult<Object>
     */
    @GetMethod("/multipart/check")
    public Return<?> checkFileUploadedByMd5(@RequestParam String md5, @RequestParam String filename, @RequestParam String bucket) {
        logger.info("REST: 通过查询 <{}> 文件是否存在", md5);
        if (Verification.checkNull(md5)) {
            logger.error("查询文件是否存在、入参无效");
            return Return.T(false, "参数无效");
        }
        return minioUploadService.checkFileMd5(md5, filename, bucket);
    }

    /**
     * 分片初始化
     *
     * @param fileUploadInfo 文件信息
     * @return ResponseResult<Object>
     */
    @PostMethod("/multipart/init")
    public Return<?> initMultiPartUpload(@RequestParam FileUploadInfo fileUploadInfo) {
        logger.info("REST: 通过 <{}> 初始化上传任务", fileUploadInfo);
        return minioUploadService.initMultiPartUpload(fileUploadInfo);
    }

    @PostMethod("/saveFileInfo")
    public Return<?> saveFileInfo(@RequestParam FileUploadInfo fileUploadInfo) {
        minioUploadService.saveUploadFileInfo(fileUploadInfo);
        return Return.T(true, "文件上传成功");
    }

    /**
     * 完成上传
     *
     * @param fileUploadInfo 文件信息
     * @return ResponseResult<Object>
     */
    @PostMethod("/multipart/merge")
    public Return<?> completeMultiPartUpload(@RequestParam FileUploadInfo fileUploadInfo) {
        logger.info("REST: 通过 {} 合并上传任务", fileUploadInfo);
        //合并文件
        String url = minioUploadService.mergeMultipartUpload(fileUploadInfo);
        //获取上传文件地址
        if (Verification.checkNotNull(url)) {
            return Return.T(url);
        }
        return Return.T(false, "文件合并失败");
    }

    public static boolean inReferer(String referer) {
        final String refererWhiteList = ProjectUtils.getSettingByRedis("intercept", "get.file.referer.white.list", "");
        if (Verification.checkNull(refererWhiteList)) return true;
        return Arrays.stream(StringUtils.split(refererWhiteList, ",")).mapToInt((e) -> Pattern.matches("^(http|https)://(.*?)" + e + "(.*?)", referer) ? 1 : 0).sum() > 0;
    }
}